classdef dotsTheScreen < dotsAllSingletonObjects
    % @class dotsTheScreen
    % Singleton to work with the Psychtoolbox Screen() function.
    % @details
    % dotsTheScreen deals with the Screen() function of Psychtoolbox.  It
    % organizes many Psychtoolbox Screen preferences as well as arguments
    % to the Screen('OpenWindow', ...) subfunction.  Preference values come
    % from dotsTheMachineConfiguration.
    % @details
    % dotsTheScreeen automatically opts in to share many several properties
    % via dotsTheSwitchboard, including the display frame rate and current
    % Psychtooolbox window number.
    % @details
    % dotsTheDrawablesManager uses dotsTheScreen to open and close drawing
    % windows.  It also coordinates remote behaviors on behalf of
    % dotsTheScreen.
    
    properties
        % dispay width (cm)
        width;
        
        % dispay height (cm)
        height;
        
        % display viewing distance (cm)
        distance;
        
        % rectangle representing the entire display (pixels, [0 0 w h])
        displayPixels;
        
        % location on display to show stimuli (pixels, [x1, y1, x2, y2])
        displayRect;
        
        % index of display connected to this machine (0-based index)
        displayIndex;
        
        % filename with path to .mat file that contains a gamma table
        gammaTableFile;
        
        % color depth of each pixel (bits)
        pixelSize;
        
        % Psychtoolbox priority value, to get extra CPU time (integer)
        priority;
        
        % Psychtoolbox stereo mode number (integer)
        stereomode;
        
        % anti-aliasing reduyndant sampling factor (integer)
        multisample;
        
        % Struct of Psychtoolbox Screen "Preference" names and values
        % See Screen("Preference?") for details.
        preferences = struct([]);
        
        % Struct of Psychtoolbox text settings and values
        % See the several Screen("Text*") functions for details.
        textSettings = struct([]);
        
        % cell array of strings of available Psychtoolbox text modes
        allTextModes = {};
        
        % Psychtoolbox Scxreen index of the current drawing window
        % @details
        % Automatically opts in to share via dotsTheSwitchboard.
        windowNumber;
        
        % pixel dimensions of the current drawing window [x y x2 y2]
        % @details
        % Automatically opts in to share via dotsTheSwitchboard.
        windowRect;
        
        % Hz frame rate of the current drawing window
        % @details
        % Automatically opts in to share via dotsTheSwitchboard.
        windowFrameRate;
        
        % degrees-to-pixels factor for the current drawing window
        % @details
        % Automatically opts in to share via dotsTheSwitchboard.
        pixelsPerDegree;
        
        % a foreground color, [L] [LA] [RGB] [RGBA], 0-255
        % @details
        % Automatically opts in to share via dotsTheSwitchboard.
        foregroundColor;
        
        % a background color, [L] [LA] [RGB] [RGBA], 0-255
        % @details
        % Automatically opts in to share via dotsTheSwitchboard.
        backgroundColor;

        clockFunction;
        
        % cell array of property names to share via dotsTheSwitchboard
        sharedProperties = ...
            {'windowNumber', ...
            'windowRect', ...
            'windowFrameRate', ...
            'pixelsPerDegree'};
        
        % display color calibration that was loaded on the video card at
        % startup (256 x 3 double array)
        systemGammaTable;
        
        % display color calibration to load onto the video card at for
        % drawing stimului (256 x 3 double array)
        stimulusGammaTable;
        
        % time of the last graphics frame onset (same units as clockFunction)
        lastFrameTime;
        
        % scale factor for detecting dropped frames
        frameTolerance = 1.5;
        
        % minimum length of a dropped frame
        frameSize;
    end
    
    methods (Access = private)
        function self = dotsTheScreen
            self.initialize;
        end
    end
    
    methods (Static)
        function s = theObject
            persistent self
            if isempty(self) || ~isvalid(self)
                self = dotsTheScreen;
            end
            s = self;
        end
        
        function reset
            self = dotsTheScreen.theObject;
            self.initialize;
        end
        
        function g = gui
            self = dotsTheScreen.theObject;
            g = [];
            disp('have not yet made gui for dotsTheScreen')
        end
        
        function hostFile = getHostGammaTableFilename
            [stat,h] = unix('hostname -s');
            hostFile = sprintf('dots_%s_GammaTable.mat', deblank(h));
        end

        function isValid = isWindowOrTexture(pointer)
            isValid = ~isempty(pointer) ...
                && pointer >= 0 ...
                && any(pointer == Screen('Windows'));
        end
    end
    
    methods
        function initialize(self)
            % initialize property values
            dotsTheMachineConfiguration.applySettingsToObject(self);
            
            % special values to share with dotsTheDrawablesManager and
            % dotsDrawables objects
            dotsTheSwitchboard.shareManyPropertiesOfObject( ...
                self.sharedProperties, self);
            
            % other values to get/set with the switchboard
            dotsTheSwitchboard.sharePropertyOfObject('foregroundColor', self);
            dotsTheSwitchboard.sharePropertyOfObject('backgroundColor', self);
            dotsTheSwitchboard.sharePropertyOfObject('clockFunction', self);

            % may not start out with an open window
            self.close;
            self.lastFrameTime = [];
            
            % pixels of entire display
            [xPix, yPix] = Screen('WindowSize', self.displayIndex);
            self.displayPixels = [0 0 xPix yPix];
            
            % share the new pixel scale factor property value
            ppd = xPix / (2*(180/pi)*atan2(self.width/2, self.distance));
            self.pixelsPerDegree = ppd;
            dotsTheSwitchboard.setSharedPropertyValue('pixelsPerDegree', ppd);
            
            % set Psychtoolbox Screen preferences
            prefs = fieldnames(self.preferences);
            for ii = 1:length(prefs)
                Screen('Preference', ...
                    prefs{ii}, self.preferences.(prefs{ii}));
            end
            
            % report on available text modes
            self.allTextModes = Screen('TextModes');
            
            % locate a gamma table
            hostFile = dotsTheScreen.getHostGammaTableFilename;
            if exist(self.gammaTableFile, 'file')
                % reload present gamma table
                self.gammaTableFromMatFile(self.gammaTableFile);
            elseif exist(hostFile, 'file')
                % reload host's default gamma table
                self.gammaTableFromMatFile(hostFile);
            else
                % no gamma table
                self.stimulusGammaTable = [];
                self.gammaTableFile = '';
            end
        end
        
        function open(self)
            % redundant opens should be OK
            openWindows = Screen('Windows');
            if ~isempty(openWindows) && ~isempty(self.windowNumber) ...
                    && any(openWindows == self.windowNumber)
                Screen('Close', self.windowNumber);
            end
            
            % request extra CPU time for Matlab
            Priority(self.priority);
            
            % Two Screen() parameters are not configurable:
            %   number of buffers is always 2
            %   never use Psychtoolbox's imaging pipeline (would be
            %   last parameter)
            [wn, rect] = Screen('OpenWindow', ...
                self.displayIndex, ...
                self.backgroundColor, ...
                self.displayRect, ...
                self.pixelSize, ...
                2, ...
                self.stereomode, ...
                self.multisample);
            self.windowNumber = wn;
            dotsTheSwitchboard.setSharedPropertyValue('windowNumber', wn);
            self.windowRect = rect;
            dotsTheSwitchboard.setSharedPropertyValue('windowRect', rect);
            
            % share the new frame rate property value
            interval = Screen('GetFlipInterval', wn);
            hz = 1/interval;
            self.windowFrameRate = hz;
            self.frameSize = self.frameTolerance * interval;
            dotsTheSwitchboard.setSharedPropertyValue('windowFrameRate', hz);
            
            % always enable texture blending
            Screen('BlendFunction', wn, ...
                GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
            
            % apply text settings to new window
            self.textSettings = self.textSettings;
            
            % optional specially-calibrated stimulus gamma table
            if ~isempty(self.stimulusGammaTable)
                oldGammaTable = Screen('LoadNormalizedGammaTable', ...
                    self.displayIndex, ...
                    self.stimulusGammaTable);
                
                if isempty(self.systemGammaTable)
                    self.systemGammaTable = oldGammaTable;
                end
            end
            HideCursor;
            
            self.shareSharedProperties;
        end
        
        function close(self)
            ShowCursor;
            Screen('CloseAll');
            self.windowNumber = -1;
            dotsTheSwitchboard.setSharedPropertyValue('windowNumber', -1);
            if ~isempty(self.systemGammaTable)
                Screen('LoadNormalizedGammaTable', ...
                    self.displayIndex, ...
                    self.systemGammaTable);
            end
            
            % request normal amount of CPU time for Matlab
            Priority(0);
            
            self.shareSharedProperties;
        end
        
        function set.textSettings(self, textSettings)
            self.textSettings = textSettings;
            if self.isWindowOrTexture(self.windowNumber);
                settings = fieldnames(textSettings);
                for ii = 1:length(settings)
                    settingValue = self.textSettings.(settings{ii});
                    if isempty(settingValue)
                        % read the default value
                        self.textSettings.(settings{ii}) = ...
                            Screen(settings{ii}, self.windowNumber);
                        
                    else
                        % set the given value
                        Screen(settings{ii}, ...
                            self.windowNumber, settingValue);
                    end
                end
            end
        end
        
        function shareSharedProperties(self)
            % special properties to share via dotsTheSwitchboard
            shared = self.sharedProperties;
            for ii = 1:length(shared)
                dotsTheSwitchboard.setSharedPropertyValue(shared{ii}, self.(shared{ii}));
            end
        end
        
        function when = nextFrame(self, dontClear)
            if self.windowNumber > 0
                if nargin < 2
                    dontClear = 0;
                end
                when = Screen('Flip', self.windowNumber, [], double(dontClear));
            else
                when = [];
            end
            self.lastFrameTime = when;
        end
        
        function gammaTableToMatFile(self, fileWithPath)
            if ischar(fileWithPath) && ~isempty(fileWithPath)
                gammaTable = self.stimulusGammaTable;
                save(fileWithPath, gammaTable);
            end
        end
        
        function gammaTableFromMatFile(self, fileWithPath)
            if ischar(fileWithPath) && ~isempty(fileWithPath)
                fileWithPath = which(fileWithPath);
                load(fileWithPath);
                self.stimulusGammaTable = gammaTable;
                self.gammaTableFile = fileWithPath;
            end
        end
    end
end